package org.gauth4me.core;

/**
 * Implements SHA-1 hash algorithm.
 *
 * Referenced by <a href="http://tools.ietf.org/html/rfc3174">RFC 3174</a>.
 */
class Sha implements HashFunction {
  private static final int BLOCK_SIZE = 64;
  private static final int HASH_SIZE = 20;

  // SHA-1 magic numbers
  private static final int initH0 = 0x67452301;
  private static final int initH1 = 0xEFCDAB89;
  private static final int initH2 = 0x98BADCFE;
  private static final int initH3 = 0x10325476;
  private static final int initH4 = 0xC3D2E1F0;
  private static final int K1 = 0x5A827999;
  private static final int K2 = 0x6ED9EBA1;
  private static final int K3 = 0x8F1BBCDC;
  private static final int K4 = 0xCA62C1D6;

  /**
   * Get an instance of SHA-1 calculator.
   */
  public Sha() {
    // nothing
  }

  public byte[] hash(byte[] data) {
    final int[] blocks = padAndCompress(data);
    final int[] w = new int[80];
    int H0 = initH0,
        H1 = initH1,
        H2 = initH2,
        H3 = initH3,
        H4 = initH4,
        a, b, c, d, e, temp;
    for (int i = 0; i < blocks.length; i += 16) {
      System.arraycopy(blocks, i, w, 0, 16);
      for (int t = 16; t < 80; ++t) {
        w[t] = rol(w[t - 3] ^ w[t - 8] ^ w[t - 14] ^ w[t - 16], 1);
      }
      a = H0;
      b = H1;
      c = H2;
      d = H3;
      e = H4;
      for (int t = 0; t < 80; ++t) {
        temp = rol(a, 5) + e + w[t];
        if (t < 20) {
          temp += K1 + ((b & c) | (~b & d));
        }
        else if (t < 40) {
          temp += K2 + (b ^ c ^ d);
        }
        else if (t < 60) {
          temp += K3 + ((b & c) | (c & d) | (b & d));
        }
        else {
          temp += K4 + (b ^ c ^ d);
        }
        e = d;
        d = c;
        c = rol(b, 30);
        b = a;
        a = temp;
      }
      H0 += a;
      H1 += b;
      H2 += c;
      H3 += d;
      H4 += e;
    }
    return split(H0, H1, H2, H3, H4);
  }

  public int getBlockSize() {
    return BLOCK_SIZE;
  }

  public int getHashSize() {
    return HASH_SIZE;
  }
  
  /**
   * Pads a byte array according to SHA-1 algorithm,
   * and compresses it into an array of int.
   *
   * @param data the array to be padded
   * @return padded and compressed array
   */
  private static int[] padAndCompress(byte[] data) {
    final int freeTrailingBytes = 64 - data.length % 64;
    final int blockCount = ceilDiv(data.length, 64)
                           + (freeTrailingBytes < 8 || freeTrailingBytes == 64 ? 1 : 0);
    final int fullWordCount = data.length / 4;
    final int remainingBytes = data.length % 4;
    int[] result = new int[blockCount*16];
    int i, j;
    for (i = 0, j = 0; j < fullWordCount; i += 4, ++j) {
      result[j] = weld(data[i], data[i + 1], data[i + 2], data[i + 3]);
    }
    switch (remainingBytes) {
      case 0:
        result[fullWordCount] = 0x80000000;
        break;
      case 1:
        result[fullWordCount] = weld(data[i], (byte) 0x80, (byte) 0x00, (byte) 0x00);
        break;
      case 2:
        result[fullWordCount] = weld(data[i], data[i + 1], (byte) 0x80, (byte) 0x00);
        break;
      case 3:
        result[fullWordCount] = weld(data[i], data[i + 1], data[i + 2], (byte) 0x80);
        break;
    }
    result[result.length - 1] = data.length * 8;
    return result;
  }

  /**
   * Welds four bytes into a single int.
   * <p/>
   * E.g. {@code weld(0x12, 0x34, 0x56, 0x78)} returns {@code 0x12345678}.
   *
   * @param b0 the first byte
   * @param b1 the second byte
   * @param b2 the third byte
   * @param b3 the last byte
   * @return welded int
   */
  private static int weld(byte b0, byte b1, byte b2, byte b3) {
    return (((int) b0 & 0xFF) << 24)
         | (((int) b1 & 0xFF) << 16)
         | (((int) b2 & 0xFF) << 8)
         | (((int) b3 & 0xFF));
  }

  /**
   * Performs a left cyclic bit shift of number by bitCount.
   *
   * @param number   the number to be shifted
   * @param bitCount the shift magnitude
   * @return number shifted bitCount to the left cyclically
   */
  private static int rol(int number, int bitCount) {
    return (number << bitCount) | (number >>> (32 - bitCount));
  }

  /**
   * Splits five 32-bit numbers into 20-byte array.
   * <p/>
   * E.g. {@code split(0x12345678, ...)}
   * results into {@code new byte[] {0x12, 0x34, 0x56, 0x78, ...}}.
   *
   * @param H0 the first number
   * @param H1 the second number
   * @param H2 the third number
   * @param H3 the fourth number
   * @param H4 the fifth number
   * @return resulting split array
   */
  private static byte[] split(int H0, int H1, int H2, int H3, int H4) {
    byte[] result = new byte[20];
    for (int i = 3; i >= 0; --i) {
      result[i     ] = (byte) (H0 & 0xFF);  H0 >>= 8;
      result[i +  4] = (byte) (H1 & 0xFF);  H1 >>= 8;
      result[i +  8] = (byte) (H2 & 0xFF);  H2 >>= 8;
      result[i + 12] = (byte) (H3 & 0xFF);  H3 >>= 8;
      result[i + 16] = (byte) (H4 & 0xFF);  H4 >>= 8;
    }
    return result;
  }

  /**
   * Calculates integral ceil of the division.
   *
   * @param dividend the dividend <em>d</em>
   * @param divisor  the divisor <em>s</em>
   * @return such minimal quotient <em>q</em> that q×s ≥ d */
  private static int ceilDiv(int dividend, int divisor) {
    return (dividend + divisor - 1) / divisor;
  }
}
